using System;
using System.Text;
using System.IO;
using System.Threading;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Xml;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace MediaBackup
{
    public class Tivo
    {
        #region Inners

        // BrianSp: Allows us to pass a predefined name and password in cleartext for HTTP Basic Authentication.  Not a good idea
        // without SSL (HTTPS).
        private class BasicCredentials : ICredentials
        {
            private string Name;
            private string Password;

            public BasicCredentials(string name, string password)
            {
                Name = name;
                Password = password;
            }

            public NetworkCredential GetCredential(Uri uri, string authType)
            {
                return new NetworkCredential(Name, Password);
            }
        }
        #endregion

        #region Static Methods
        private static void TimeoutCallback(object state, bool timedOut)
        {
            Tivo dl = (Tivo)state;
            dl.TimeoutCallback(timedOut);
        }

        // BrianSp: Callback to allow any cert for SSL, no matter whether it can be proved valid or not.
        private static bool IgnoreCertErrorsCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
            return true;
        }

        // BrianSp: Callbacks are typically static, and take a context object (AsyncState in this case).  I almost
        // always use that context to move right back into instance method territory.
        private static void StaticResponseCallback(IAsyncResult result)
        {
            Tivo dl = (Tivo)result.AsyncState;
            dl.ResponseCallback(result);
        }

        private static void StaticListReadCallback(IAsyncResult result)
        {
            Tivo dl = (Tivo)result.AsyncState;
            dl.ListReadCallback(result);
        }
        #endregion

        #region Instance Data

        // BrianSp: Allows us to propagate errors across thread and async IO boundaries.
        private Exception m_LastError;

        // $$BrianSp: Poor encapsulation, revisit these.
        // BrianSp: Keep one settings per TivoClient (of which there are one per main thread), so they can have independent
        // values for each main thread.
        public Stream m_ResponseStream;
        public byte[] m_Buffer = new byte[0x10000];

        private Settings m_Settings;

        // BrianSp: Held on behalf of callers of SendRequest so that it can be passed to the eventual call to m_ResponseStream.BeginRead.
        private AsyncCallback m_ReadCallback;
        private object m_ReadContext;

        // BrianSp: Held on behalf of the current request frame so it can be used later, across thread and async IO boundaries.
        private ManualResetEvent m_Wait;
        private HttpWebRequest m_Request;
        private HttpWebResponse m_Response;

        // BrianSp: Holds the current TiVo session identifier (sid) cookie value.
        private string m_Cookie;

        // BrianSp: Buffers the response to TiVo QueryContainer requests for eventual parsing as complete xml.
        private StringBuilder m_ResponseString;
        #endregion

        #region Instance Methods
        public Tivo(Settings settings)
        {
            m_Settings = settings;

            // BrianSp: When talking to the TiVo, the cert is not able to be validated.  This is fine, we really don't care, so tell
            // the system to ignore cert errors.
            ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(IgnoreCertErrorsCallback);
        }

        private void TimeoutCallback(bool timedOut)
        {
            if (timedOut)
            {
                Log.WriteLine("Timed out!");
                m_Request.Abort();
            }
        }

        // BrianSp: This blocking method downloads all of the containers (folders) on your TiVo, and turns any recorded shows found into persisted
        // MediaBackup.Item instances.
        public void Discover()
        {
            try
            {
                DiscoverUri("https://" + m_Settings.TivoIpAddress + "/TiVoConnect?Command=QueryContainer&Container=%2FNowPlaying");
            }
            finally
            {
                Dispose();
            }
        }

        private void DiscoverUri(string uri)
        {
            WaitHandle wait = SendRequest(uri, new AsyncCallback(StaticListReadCallback), this);
            CompleteRequest(wait);
        }

        public WaitHandle SendRequest(string uri, AsyncCallback readCallback, object context)
        {
            m_ReadCallback = readCallback;
            m_ReadContext = context;

            // BrianSp: Clear a bunch of request specific transient state.
            m_LastError = null;
            m_ResponseStream = null;
            m_ResponseString = new StringBuilder();
            m_Wait = new ManualResetEvent(false);

            m_Request = (HttpWebRequest)WebRequest.Create(uri);
            m_Request.Credentials = new BasicCredentials("tivo", m_Settings.TivoMak);
            m_Request.PreAuthenticate = true;

            // BrianSp: We may have a session identified (sid) cookie value from a previous QueryContainer call.  It must be present
            // to download an actual recorded show.
            if (m_Cookie != null)
            {
                string[] items = m_Cookie.Split(';');
                foreach (string item in items)
                {
                    string trimmed = item.Trim();
                    string[] parts = trimmed.Split('=');
                    if (parts.Length == 2 && parts[0] == "sid")
                        m_Request.Headers.Add("Cookie", trimmed);
                }
            }

            // BrianSp: m_Wait is frame-specific.  Since we may asynchronously recurse immediately after calling BeginGetResponse, we need to
            // store it on this thread's stack frame so we can return the right value at the end of this method.
            ManualResetEvent wait = m_Wait;

            IAsyncResult result = m_Request.BeginGetResponse(new AsyncCallback(StaticResponseCallback), this);
            ThreadPool.RegisterWaitForSingleObject(result.AsyncWaitHandle, new WaitOrTimerCallback(TimeoutCallback), this, 30000, true);

            return wait;
        }

        // BrianSp: In order to treat multi-threaded asynchronous operations as single-threaded blocking operations.
        public void CompleteRequest(WaitHandle wait)
        {
            wait.WaitOne();
            if (m_LastError != null)
                throw new Exception("Error from another thread", m_LastError);
        }

        private void ResponseCallback(IAsyncResult result)
        {
            ManualResetEvent wait = m_Wait;

            try
            {
                m_Response = (HttpWebResponse)m_Request.EndGetResponse(result);

                // BrianSp: At this point we have completed downloading the http headers.  Get the cookie if there is one.
                string cookie = m_Response.Headers["Set-Cookie"];
                if (cookie != null)
                    m_Cookie = cookie;

                // BrianSp: Now we can kick off asynchronous download of the http response body.
                m_ResponseStream = m_Response.GetResponseStream();
                m_ResponseStream.BeginRead(m_Buffer, 0, m_Buffer.Length, m_ReadCallback, m_ReadContext);
            }
            catch (Exception e)
            {
                m_LastError = e;
                Dispose(wait);
            }
        }

        private void ListReadCallback(IAsyncResult result)
        {
            ManualResetEvent wait = m_Wait;

            try
            {
                int read = m_ResponseStream.EndRead(result);
                if (read > 0)
                {
                    // BrianSp: Reading bytes indicates the response is not finished.  Buffer the current chunk and kick off another read.
                    m_ResponseString.Append(Encoding.UTF8.GetString(m_Buffer, 0, read));
                    m_ResponseStream.BeginRead(m_Buffer, 0, m_Buffer.Length, new AsyncCallback(StaticListReadCallback), this);
                }
                else
                {
                    m_ResponseStream.Close();
                    m_ResponseStream.Dispose();
                    m_ResponseStream = null;

                    // BrianSp: At this point we should have a complete xml QueryContainer response.  Load it up in an XML DOM and process the results.
                    XmlDocument doc = new XmlDocument();
                    doc.Load(new StringReader(m_ResponseString.ToString()));

                    // BrianSp: Xml namespaces leave me tired and confused.
                    XmlAttribute xmlns = doc.DocumentElement.Attributes["xmlns"];
                    if (xmlns.Value != "http://www.tivo.com/developer/calypso-protocol-1.6/")
                        Log.Write(new Exception("Warning! TiVo protocol may have changed: " + xmlns.Value));

                    XmlNamespaceManager ns = new XmlNamespaceManager(doc.NameTable);
                    ns.AddNamespace("n", xmlns.Value);

                    XmlNodeList list = doc.SelectNodes("//n:Item", ns);
                    foreach (XmlNode node in list)
                    {
                        XmlNode url = node.SelectSingleNode("n:Links/n:Content/n:Url", ns);
                        XmlNode contentType = node.SelectSingleNode("n:Links/n:Content/n:ContentType", ns);

                        if(contentType.InnerText == "video/x-tivo-raw-tts")
                        {
                            XmlNode inProgress = node.SelectSingleNode("n:Details/n:InProgress", ns);
                            if (inProgress != null)
                            {
                                // BrianSp: TiVo won't allow downloading of in-progress recordings, which is fine.  Skip it for now,
                                // it should get picked up when it eventually completes recording.
                                if (inProgress.InnerText == "Yes")
                                    continue;
                            }

                            XmlNode title = node.SelectSingleNode("n:Details/n:Title", ns);
                            XmlNode episode = node.SelectSingleNode("n:Details/n:EpisodeTitle", ns);
                            XmlNode size = node.SelectSingleNode("n:Details/n:SourceSize", ns);

                            // BrianSp: This creates the item, and saves its meta-data to disk.  It will get picked up later for pipeline download and processing.
                            Item.Create(title.InnerText, episode == null ? null : episode.InnerText, ulong.Parse(size.InnerText), url.InnerText, this, m_Settings);
                        }
                        else if(contentType.InnerText == "x-tivo-container/folder")
                        {
                            // BrianSp: Asynchronous recursion, what fun.
                            DiscoverUri(url.InnerText);
                        }
                    }

                    Dispose(wait);
                }
            }
            catch (Exception e)
            {
                m_LastError = e;
                Dispose(wait);
            }
        }

        public void Dispose()
        {
            Dispose(m_Wait);
        }

        private void Dispose(ManualResetEvent wait)
        {
            if (m_ResponseStream != null)
            {
                m_ResponseStream.Close();
                m_ResponseStream.Dispose();
                m_ResponseStream = null;
            }

            if(wait != null)
                wait.Set();
        }
        #endregion
    }
}
